package jmine.tec.rpc.server.representation;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jmine.tec.rpc.server.representation.representers.collections.ListRepresenterBean;
import jmine.tec.rpc.server.representation.representers.collections.MapRepresenterBean;
import jmine.tec.rpc.server.representation.representers.collections.SetRepresenterBean;
import jmine.tec.utils.collection.impl.CopyOnWriteHashMap;
import jmine.tec.utils.register.Receiver;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

/**
 * @author lundberg
 */
public class BaseRepresentationFactory implements RepresentationFactory, Receiver<Representer<?, ?>> {

    private FallbackRepresenterFactory fallback;

    private final Map<RepresenterKey, Representer<?, ?>> representers = new CopyOnWriteHashMap<RepresenterKey, Representer<?, ?>>();

    /**
     * {@inheritDoc}
     */
    public <R, E> R representObject(Class<R> representationClass, E object) {
        return this.representObject(new RepresentationContext(this), representationClass, object);
    }

    /**
     * {@inheritDoc}
     */
    public <R, E> List<R> representObjects(Class<R> representationClass, List<E> objects) {
        return this.representObjects(new RepresentationContext(this), representationClass, objects);
    }

    /**
     * {@inheritDoc}
     */
    public <R, E> Set<R> representObjects(Class<R> representationClass, Set<E> objects) {
        return this.representObjects(new RepresentationContext(this), representationClass, objects);
    }

    /**
     * {@inheritDoc}
     */
    public <RK, RV, EK, EV> Map<RK, RV> representObjects(Class<RK> keyClass, Class<RV> valueClass, Map<EK, EV> objects) {
        return this.representObjects(new RepresentationContext(this), keyClass, valueClass, objects);
    }

    /**
     * Cria uma representação do objeto, de acordo com o tipo de representação desejada.
     * 
     * @param <R> tipo de representação
     * @param <E> tipo de objeto
     * @param context context
     * @param representationClass representationClass
     * @param object object
     * @return representation
     */
    <R, E> R representObject(RepresentationContext context, Class<R> representationClass, E object) {
        return object == null ? null : this.findRepresenterFor(representationClass, object).represent(context, object);
    }

    /**
     * Cria representações dos objetos, de acordo com o tipo de representação desejada.
     * 
     * @param <R> tipo de representação
     * @param <E> tipo de objeto
     * @param context context
     * @param representationClass representationClass
     * @param objects objects
     * @return representation
     */
    @SuppressWarnings("unchecked")
    <R, E> List<R> representObjects(RepresentationContext context, Class<R> representationClass, List<E> objects) {
        return objects == null ? null : this.representObject(context, List.class, new ListRepresenterBean(representationClass, objects));
    }

    /**
     * Cria representações dos objetos, de acordo com o tipo de representação desejada.
     * 
     * @param <R> tipo de representação
     * @param <E> tipo de objeto
     * @param context context
     * @param representationClass representationClass
     * @param objects objects
     * @return representation
     */
    @SuppressWarnings("unchecked")
    <R, E> Set<R> representObjects(RepresentationContext context, Class<R> representationClass, Set<E> objects) {
        return objects == null ? null : this.representObject(context, Set.class, new SetRepresenterBean(representationClass, objects));
    }

    /**
     * Cria representações dos objetos, de acordo com o tipo de representação desejada.
     * 
     * @param <RK> tipo de representação chave
     * @param <RV> tipo de representação valor
     * @param <EK> tipo de objeto chave
     * @param <EV> tipo de objeto valor
     * @param context context
     * @param keyClass keyClass
     * @param valueClass valueClass
     * @param objects objects
     * @return representation
     */
    @SuppressWarnings("unchecked")
    <RK, RV, EK, EV> Map<RK, RV> representObjects(RepresentationContext context, Class<RK> keyClass, Class<RV> valueClass,
            Map<EK, EV> objects) {
        return objects == null ? null : this.representObject(context, Map.class, new MapRepresenterBean(keyClass, valueClass, objects));
    }

    /**
     * Encontra um representador apropriado para o objeto.
     * 
     * @param <R> tipo de representação
     * @param <E> tipo de objeto
     * @param representationClass representationClass
     * @param object object
     * @return Representer
     */
    @SuppressWarnings("unchecked")
    private <R, E> Representer<R, E> findRepresenterFor(Class<R> representationClass, E object) {
        Class<? extends Object> fromType = object.getClass();
        RepresenterKey key = this.createKey(representationClass, fromType);
        Representer<?, ?> representer = representers.get(key);
        if (representer == null) {
            representer = findRepresenterRecurse(representationClass, fromType, fromType);
            this.representers.put(key, representer);
        }
        return (Representer<R, E>) representer;
    }

    /**
     * Procura um {@link Representer} para o tipo passada, subindo na hierarquia caso nao seja encontrado
     * 
     * @param representationType {@link Class}
     * @param fromType {@link Class}
     * @param originalType {@link Class}
     * @return {@link Representer}
     */
    private Representer<?, ?> findRepresenterRecurse(Class<?> representationType, Class<?> fromType, Class<?> originalType) {
        if (fromType == null) {
            return this.fallback.create(representationType, originalType);
        }
        RepresenterKey key = this.createKey(representationType, fromType);
        Representer r = this.representers.get(key);
        if (r != null) {
            return r;
        }
        return findRepresenterRecurse(representationType, fromType.getSuperclass(), originalType);
    }

    /**
     * {@inheritDoc}
     */
    public void register(Collection<? extends Representer<?, ?>> registers) {
        for (Representer<?, ?> representer : registers) {
            this.register(representer);
        }
    }

    /**
     * Registra um Representer
     * 
     * @param representer representer
     */
    protected void register(Representer<?, ?> representer) {
        this.representers.put(this.createKey(representer.getRepresentationClass(), representer.getRepresentedClass()), representer);
    }

    /**
     * @param representationClass representationClass
     * @param representedClass representedClass
     * @return RepresenterKey
     */
    protected RepresenterKey createKey(Class<?> representationClass, Class<?> representedClass) {
        return new RepresenterKey(representationClass, representedClass);
    }

    /**
     * Chave de um representador
     * 
     * @author lundberg
     */
    private static class RepresenterKey {
        private final Class<?> representationClass;

        private final Class<?> representedClass;

        private int hashCode;

        /**
         * @param representationClass representationClass
         * @param representedClass representedClass
         */
        public RepresenterKey(Class<?> representationClass, Class<?> representedClass) {
            this.representationClass = representationClass;
            this.representedClass = representedClass;
            this.hashCode = new HashCodeBuilder().append(this.representationClass).append(this.representedClass).toHashCode();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof RepresenterKey)) {
                return false;
            }
            RepresenterKey o = (RepresenterKey) obj;
            return new EqualsBuilder().append(this.representationClass, o.representationClass).append(this.representedClass,
                    o.representedClass).isEquals();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            return this.hashCode;
        }
    }

    /**
     * @param fallback the fallback to set
     */
    public void setFallback(FallbackRepresenterFactory fallback) {
        this.fallback = fallback;
    }
}
